/**
 * Copyright (c) iwindplus Technologies Co., Ltd.2024-2030, All rights reserved.
 */

package com.iwindplus.boot.web.mvc.handler;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.extra.servlet.JakartaServletUtil;
import com.iwindplus.boot.domain.constant.CommonConstant.ExceptionConstant;
import com.iwindplus.boot.domain.constant.CommonConstant.SymbolConstant;
import com.iwindplus.boot.domain.constant.CommonConstant.TraceConstant;
import com.iwindplus.boot.domain.enums.BizCodeEnum;
import com.iwindplus.boot.domain.exception.CommonException;
import com.iwindplus.boot.domain.vo.ResultVO;
import com.iwindplus.boot.util.CryptoUtil;
import com.iwindplus.boot.util.HttpsUtil;
import com.iwindplus.boot.util.JacksonUtil;
import com.iwindplus.boot.web.domain.property.GlobalErrorProperty;
import com.iwindplus.boot.web.domain.property.ResponseBodyProperty;
import com.iwindplus.boot.web.domain.property.ResponseBodyProperty.ResponseBodyCryptoConfig;
import io.swagger.v3.oas.annotations.Hidden;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.servlet.MultipartProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.server.ServerWebInputException;

/**
 * web端全局异常处理.
 *
 * @author zengdegui
 * @since 2018/9/1
 */
@Slf4j
@Hidden
@Order(Ordered.HIGHEST_PRECEDENCE)
@RestControllerAdvice
@EnableConfigurationProperties(GlobalErrorProperty.class)
@ConditionalOnProperty(prefix = "global.error", name = "enabled", havingValue = "true", matchIfMissing = true)
public class GlobalErrorHandler {

    @Resource
    private GlobalErrorProperty globalErrorProperty;

    @Resource
    private ResponseBodyProperty responseBodyProperty;

    @Resource
    private MultipartProperties multipartProperties;

    /**
     * 全局异常捕获.
     *
     * @param ex      异常
     * @param request 请求
     * @return ResponseEntity<Object>
     */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Object> exception(Exception ex, HttpServletRequest request) {
        final String uri = request.getRequestURI();
        final String realIp = Optional.ofNullable(MDC.get(TraceConstant.REAL_IP)).orElse(JakartaServletUtil.getClientIP(request));
        log.info("GlobalErrorHandler全局异常捕获, uri={}, realIp={}", uri, realIp);

        ResultVO<?> result;
        if (ex instanceof CommonException exception) {
            log.warn("GlobalErrorHandler全局异常捕获，message={}", ex.getMessage(), ex);

            result = ResultVO.error(exception);
        } else {
            log.error("GlobalErrorHandler全局异常捕获，message={}", ex.getMessage(), ex);

            final String className = ex.getClass().getName();
            result = this.getException(ex, className).getBody();
        }
        return GlobalErrorHandler.responseData(globalErrorProperty, responseBodyProperty, result);
    }

    /**
     * 响应数据.
     *
     * @param globalErrorProperty  全局异常配置
     * @param responseBodyProperty 全局响应配置
     * @param result               响应实体
     * @return ResponseEntity<Object>
     */
    private static ResponseEntity<Object> responseData(GlobalErrorProperty globalErrorProperty,
        ResponseBodyProperty responseBodyProperty, ResultVO<?> result) {
        HttpStatusCode httpStatusCode = GlobalErrorHandler.getHttpStatusCode(globalErrorProperty, null, result);
        final Object data = GlobalErrorHandler.getResult(responseBodyProperty, result);
        return ResponseEntity.status(httpStatusCode).body(data);
    }

    /**
     * 响应数据.
     *
     * @param response             响应
     * @param globalErrorProperty  全局异常配置
     * @param responseBodyProperty 全局响应配置
     * @param httpStatusCode       状态码
     * @param result               响应实体
     */
    public static void responseData(HttpServletResponse response, GlobalErrorProperty globalErrorProperty,
        ResponseBodyProperty responseBodyProperty, HttpStatusCode httpStatusCode, ResultVO<?> result) {
        httpStatusCode = GlobalErrorHandler.getHttpStatusCode(globalErrorProperty, httpStatusCode, result);
        final Object data = GlobalErrorHandler.getResult(responseBodyProperty, result);
        HttpsUtil.responseData(response, httpStatusCode, data);
    }

    private static HttpStatusCode getHttpStatusCode(GlobalErrorProperty globalErrorProperty, HttpStatusCode httpStatusCode, ResultVO<?> result) {
        // 启用异常状态码返回200
        if (Boolean.TRUE.equals(globalErrorProperty.getEnabled()) && Boolean.TRUE.equals(globalErrorProperty.getEnabledStatusOk())) {
            return HttpStatus.OK;
        }
        return Optional.ofNullable(httpStatusCode).orElse(result.getBizStatus() > HttpStatus.NETWORK_AUTHENTICATION_REQUIRED.value()
            ? HttpStatus.INTERNAL_SERVER_ERROR : HttpStatusCode.valueOf(result.getBizStatus()));
    }

    private static Object getResult(ResponseBodyProperty responseBodyProperty, ResultVO<?> result) {
        // 判断是否启用响应解密
        final ResponseBodyCryptoConfig crypto = responseBodyProperty.getCrypto();
        if (Boolean.TRUE.equals(responseBodyProperty.getEnabled()) && Boolean.TRUE.equals(crypto.getEnabled())) {
            log.info("异常响应加密数据，算法={}", crypto.getAlgorithm());
            return CryptoUtil.encrypt(JacksonUtil.toJsonStr(result), crypto);
        }
        return result;
    }

    private ResponseEntity<ResultVO<?>> getException(Exception ex, String className) {
        if (CharSequenceUtil.contains(className, ExceptionConstant.CONSTRAINT_VIOLATION_EXCEPTION)) {
            final ConstraintViolationException exs = (ConstraintViolationException) ex;
            final String message = this.getMessage(exs);
            final BizCodeEnum bizCodeEnum = BizCodeEnum.PARAM_CONSTRAINT_VIOLATION;
            final Integer bizStatus = bizCodeEnum.getBizStatus();
            final String bizCode = bizCodeEnum.getBizCode();
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.buildSourceResult(bizStatus, bizCode, message));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.METHOD_ARGUMENT_NOT_VALID_EXCEPTION)) {
            final MethodArgumentNotValidException exs = (MethodArgumentNotValidException) ex;
            final String message = this.getMessage(exs.getBindingResult());
            final BizCodeEnum bizCodeEnum = BizCodeEnum.PARAM_INVALID;
            final Integer bizStatus = bizCodeEnum.getBizStatus();
            final String bizCode = bizCodeEnum.getBizCode();
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.buildSourceResult(bizStatus, bizCode, message));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.BIND_EXCEPTION)) {
            final BindException exs = (BindException) ex;
            final String message = this.getMessage(exs.getBindingResult());
            final BizCodeEnum bizCodeEnum = BizCodeEnum.PARAM_BIND_ERROR;
            final Integer bizStatus = bizCodeEnum.getBizStatus();
            final String bizCode = bizCodeEnum.getBizCode();
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.buildSourceResult(bizStatus, bizCode, message));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.MULTIPART_EXCEPTION)) {
            Object[] bizMessageParams = Optional.ofNullable(this.multipartProperties.getMaxFileSize()).map(data -> new Object[]{data}).orElse(null);
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.FILE_SIZE_LIMIT, bizMessageParams));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.ILLEGAL_ARGUMENT_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.PARAM_ILLEGAL));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.SERVER_WEB_INPUT_EXCEPTION)) {
            final ServerWebInputException item = (ServerWebInputException) ex;
            String parameterName = Optional.ofNullable(item).map(ServerWebInputException::getMethodParameter)
                .map(MethodParameter::getParameterName).orElse(null);
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.PARAM_INPUT_ERROR, new Object[]{parameterName}));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.MISSING_SERVLET_REQUEST_PARAMETER_EXCEPTION)) {
            final MissingServletRequestParameterException item = (MissingServletRequestParameterException) ex;
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.PARAM_MISS, new Object[]{item.getParameterName()}));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.METHOD_ARGUMENT_TYPE_MISMATCH_EXCEPTION)) {
            final MethodArgumentTypeMismatchException item = (MethodArgumentTypeMismatchException) ex;
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.PARAM_TYPE_MISMATCH, new Object[]{item.getName()}));
        }
        return this.getExceptionOne(ex, className);
    }

    private ResponseEntity<ResultVO<?>> getExceptionOne(Exception ex, String className) {
        if (CharSequenceUtil.contains(className, ExceptionConstant.UNAUTHORIZED_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ResultVO.error(HttpStatus.UNAUTHORIZED));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.FILE_NOT_FOUND_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.FILE_NOT_FOUND));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.NO_HANDLER_FOUND_EXCEPTION)
            || CharSequenceUtil.contains(className, ExceptionConstant.NO_RESOURCE_FOUND_EXCEPTION)
            || CharSequenceUtil.contains(className, ExceptionConstant.NOT_FOUND_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ResultVO.error(HttpStatus.NOT_FOUND));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.HTTP_REQUEST_METHOD_NOT_SUPPORTED_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).body(ResultVO.error(HttpStatus.METHOD_NOT_ALLOWED));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.HTTP_MEDIA_TYPE_NOT_SUPPORTED_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE).body(ResultVO.error(HttpStatus.UNSUPPORTED_MEDIA_TYPE));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.HTTP_MEDIA_TYPE_NOT_ACCEPTABLE_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE).body(ResultVO.error(HttpStatus.NOT_ACCEPTABLE));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.UNSUPPORTED_OPERATION_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.UNSUPPORTED_OPERATION));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.HTTP_MESSAGE_NOT_READABLE_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.NOT_READABLE));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.HTTP_MESSAGE_NOT_WRITABLE_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.NOT_WRITABLE));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.NULL_POINTER_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.NULL_POINTER));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.CONVERSION_NOT_SUPPORTED_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.CONVERSION_NOT_SUPPORTED));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.MISSING_SERVLET_REQUEST_PART_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.MISSING_FILE));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.CLASS_CAST_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.CLASS_CAST_ERROR));
        }
        return this.getExceptionTwo(ex, className);
    }

    private ResponseEntity<ResultVO<?>> getExceptionTwo(Exception ex, String className) {
        if (CharSequenceUtil.contains(className, ExceptionConstant.NUMBER_FORMAT_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.ONLY_SUPPORT_NUMBER));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.SECURITY_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.SECURITY_ERROR));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.BAD_SQL_GRAMMAR_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.BAD_SQL_GRAMMAR));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.SQL_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.SQL_ERROR));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.MYBATIS_SYSTEM_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.MYBATIS_ERROR));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.DATA_INTEGRITY_VIOLATION_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.DATA_INTEGRITY_VIOLATION_ERROR));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.TYPE_NOT_PRESENT_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.TYPE_NOT_PRESENT));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.IO_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.IO_ERROR));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.NO_SUCH_METHOD_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.NO_SUCH_METHOD));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.INDEX_OUT_OF_BOUNDS_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.INDEX_OUT_OF_BOUNDS));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.NO_SUCH_BEAN_DEFINITION_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.NO_SUCH_BEAN));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.TYPE_MISMATCH_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.TYPE_MISMATCH));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.STACK_OVERFLOW_ERROR)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.STACK_OVERFLOW));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.ARITHMETIC_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.ARITHMETIC_ERROR));
        }
        return this.getExceptionThree(ex, className);
    }

    private ResponseEntity<ResultVO<?>> getExceptionThree(Exception ex, String className) {
        if (CharSequenceUtil.contains(className, ExceptionConstant.MAIL_SEND_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.SEND_ERROR));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.SERIALIZATION_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.SERIALIZE_ERROR));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.JSON_PROCESSING_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.JSON_PROCESSING_ERROR));
        } else if (CharSequenceUtil.contains(className, ExceptionConstant.JSON_MAPPING_EXCEPTION)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResultVO.error(BizCodeEnum.JSON_MAPPING_ERROR));
        }

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ResultVO.error(HttpStatus.INTERNAL_SERVER_ERROR));
    }

    private String getMessage(ConstraintViolationException exs) {
        final Set<ConstraintViolation<?>> violations = exs.getConstraintViolations();
        if (CollUtil.isNotEmpty(violations)) {
            return violations.stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(SymbolConstant.SEMICOLON));
        }
        return null;
    }

    private String getMessage(BindingResult exs) {
        List<ObjectError> allErrors = exs.getAllErrors();
        if (CollUtil.isNotEmpty(allErrors)) {
            return allErrors.stream().map(s -> s.getDefaultMessage()).collect(Collectors.joining(SymbolConstant.SEMICOLON));
        }
        return null;
    }
}
